Skip to content

feat: add per-provider additional scan folders (#177) - #187

Merged
chattymin merged 2 commits into
chattymin:mainfrom
Borisserz:feat/per-provider-custom-scan-roots
Aug 22, 2026
Merged

chattymin merged 2 commits into
chattymin:mainfrom
Borisserz:feat/per-provider-custom-scan-roots

Conversation

@Borisserz

@Borisserz Borisserz commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Every provider resolves its log location on its own, and only Claude could be pointed somewhere else from inside the app. A relocated Codex / Gemini / Cursor / … directory does not error — the total is just lower, and nothing in the UI says a folder was missed.

This is the generalized form of #162, landed on main so it does not wait on that PR. The feature, the CLAUDE_CONFIG_DIR diagnosis, and defect B all originated in @hannin's #162; this PR takes that review's blocking items and extends the extra-folder list to every provider. Storage is one list per provider (customScanRoots.<id>), not one flat list every reader tries. Gemini's allowJSON walk would otherwise ingest Claude .meta.json and mis-attribute tokens.

Custom roots only add to curated defaults. An ancestor extra (~, /) is dropped so normalizedRoots cannot fold a hidden default away — jsonlFiles uses .skipsHiddenFiles, and scanning ~ silently zeros Claude. That is #162-B, baked in here. .skipsHiddenFiles is left alone.

#162 can rebase onto these keys or close. There is no generic customScanRoots key, so no migration.

Rebased onto current main. After #182, Antigravity has three default roots (2.0/Core, CLI, IDE). resolvedRoots unions custom extras onto that full list — a CLI-only literal is defaultRoots[1] and would silently drop the other two editions. Codex custom roots go through computeCodexScanRoots, so archived sessions (#181) stay in the union.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • Other:

UI changes

Before After
Advanced had no extra-folder field on main (#162's Claude-only field is still on its own branch). Settings → Advanced → Provider picker + one Additional scan folders list. Persist on submit, focus loss, provider switch, or collapsing Advanced — not per keystroke. Match count is post-fold, off the main thread, debounced.

Checklist

  • swift build and swift test pass locally
  • PR title and description are written in English
  • UI changes are described above (before/after — images optional)
  • No copyrighted assets, secrets, or private tooling references are committed (see CONTRIBUTING)
  • Tests were added or updated for this change

What this takes from the #162 review

Blocking items from the owner review on #162, applied here so they are not re-opened:

  • A. L.t(ko, en, ja, es) — four arguments, current main.
  • B. Custom roots cannot evict a curated default (~ and / included). Tests actually scan a hidden .claude/projects jsonl after that.
  • C. Key is customScanRoots.claude_code (and .codex, .gemini, …), never customScanRoots.

Recommendations 1–4: commit on submit / blur, not keystroke; match count off main thread; count after normalizedRoots; compute functions default customRootsValue: nil, not UserDefaults.standard.

French strings are left to #185 on rebase, per merge order.

Tests

CustomScanRootsTests / AntigravityUsageTests — each branch alone:

  • testAncestorCustomRootDoesNotEvictCuratedDefault / testClaudeAncestorCustomDoesNotZeroJsonlScan — the Add user-configurable additional scan folders with wildcard support #162-B zero-total. Confirmed by scanning a real jsonl under .claude/projects.
  • testRootFilesystemCustomDoesNotEvictCuratedDefault — / is an ancestor of everything; extra + \"/\" is \"//\" so a naive prefix check would walk the whole disk.
  • testEveryRegisteredProviderConsultsItsOwnCustomScanRootsKey — source sweep for storedValue(for: \"<id>\"), plus curatedRoots(for:) non-empty for every registered provider (Settings match count).
  • testNoRootScanReadsEveryDefaultRoot — one turn in each of the three Antigravity default relative paths under a fake home; resolvedRoots + entries must see all three. Confirmed red against the CLI-only literal (only r1).
  • testEntriesWithoutRootsGoesThroughResolvedRoots — production no-root path and the cache actor both call resolvedRoots.
  • testCodexSessionRootsIncludeArchivedSessions — custom-root union must not drop #181's archived root.
  • testCodexCacheKeepsSessionIndexAcrossMultipleRoots — prune after the union, not per root.
  • testCodexCacheFindsParentInASiblingRoot — parent-closure over the union.
  • testSetCustomScanRootsInvalidatesAdditionalProviderCache — OpenCode/Cursor/Copilot/Kiro 30s cache would hide a newly added folder.
  • testSettingsUsesProviderPickerAndCommitsOnSubmit — picker + one list; customScanDraftOwnerID so a picker click that blurs the field cannot write Claude paths into customScanRoots.gemini.

Full suite: 760 pass, 10 skipped, 0 failures.

How to verify

DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer \
  swift test --filter 'CustomScanRootsTests|AntigravityUsageTests.testNoRootScanReadsEveryDefaultRoot'

Manual: Settings → Advanced → pick a provider whose logs are outside the built-in path → paste that folder → submit or click away. The next refresh should pick those sessions up. Pointing Gemini at a Claude folder must not inflate Gemini. Typing ~ must not drop today's Claude total to zero.

Closes #177

@chattymin

Copy link
Copy Markdown
Owner

Thanks @Borisserz — a lot of surface handled carefully. I checked the parts that matter rather than reading them:

Blocking items A/B/C from the #162 review and recommendations 1–4 are all in.

1. Rebase — and one part of it is not mechanical

main moved: #179, #181, #182 and #184 all touch the four conflicting files. Most is textual — your own #179 changed kiroEntries to return a tuple, but the old overload is still there, so that resolves cleanly.

#182 is the one to watch. It landed multi-root discovery for Antigravity:

static var defaultRoots: [URL] {
    [ ".gemini/antigravity/conversations",      // 2.0 / Core
      ".gemini/antigravity-cli/conversations",  // CLI
      ".gemini/antigravity-ide/conversations" ] // IDE
}
static var defaultRoot: URL { defaultRoots[0] }

resolvedRoots here hardcodes one default:

defaults: [home.appendingPathComponent(".gemini/antigravity-cli/conversations")]

That is now defaultRoots[1] — not even defaultRoot. Since this PR rewrites entries(modifiedSince:) to go through resolvedRoots, resolving the conflict textually drops the 2.0/Core and IDE roots: a silent undercount for those users, which is the class this PR exists to remove. Please union against defaultRoots rather than a literal path.

2. The tests will not tell you if you miss it

I checked what would catch it. Nothing does:

test why it stays green
asserts on defaultRoots directly this PR does not change defaultRoots
defaultRoot == defaultRoots[0] unchanged
entries(..., root:) / entries(..., roots:) roots passed explicitly

Nothing calls entries(modifiedSince:) with no root and asserts all three defaults get scanned. Worth adding that one — it is the assertion that makes resolvedRoots load-bearing.

3. curatedRoots is a second registry, and it is undocumented — non-blocking

CustomScanRoots.curatedRoots(for:) switches on provider id, but testEveryRegisteredProviderConsultsItsOwnCustomScanRootsKey only sweeps for storedValue(for:), and the provider-extension.md paragraph this PR adds never mentions curatedRoots. A new provider that follows the doc lands in default: return [].

Blast radius is small: the only caller is the Settings match count (SettingsView.swift:513), and scanning stays correct because each reader passes its own defaults. But the doc now describes one of the two places a new provider has to touch. Either extend the sweep to cover it, or derive the curated roots from the registered providers so there is only one list.

4. Credit — non-blocking

This owes a fair amount to #162: the feature itself, the CLAUDE_CONFIG_DIR diagnosis behind it, and the B defect, which I only found while reviewing that PR. A line crediting @hannin in the description would be right. I will close #162 as superseded rather than rejected once this lands.

Merge order

I am taking this ahead of #185 (French UI), so you do not need to produce French for the five new strings — the French contributor will pick them up on their rebase.

Logs outside the built-in paths were a silent undercount for every
provider except Claude. Store one list per provider so parsers never
share folders, and only add custom roots — never evict a curated default.
…n#177)

resolvedRoots hardcoded the CLI path, which is defaultRoots[1] after
chattymin#182, so a no-root scan dropped 2.0/Core and IDE stores. Codex custom
roots had the same class of hole against archived_sessions (chattymin#181).
@Borisserz
Borisserz force-pushed the feat/per-provider-custom-scan-roots branch from 5705c3f to 0a7d134 Compare August 20, 2026 17:26
@Borisserz

Copy link
Copy Markdown
Contributor Author

Rebased onto current main.

  1. resolvedRoots unions custom extras onto defaultRoots(home:) — all three editions from feat: support multi-root discovery for Antigravity 2.0 and IDE #182, not the CLI path (defaultRoots[1]). testNoRootScanReadsEveryDefaultRoot seeds one turn in each relative path under a fake home; it was red against the literal (only r1) and is green after the union. The no-root entries(modifiedSince:) path and the cache actor both go through resolvedRoots.

  2. Same class on Codex: codexSessionRoots now unions computeCodexScanRoots, so archived sessions (fix: preserve Codex usage after session archiving #181) are not dropped when custom folders are set.

  3. testEveryRegisteredProviderConsultsItsOwnCustomScanRootsKey now also requires curatedRoots(for:) non-empty for every registered id. provider-extension.md names that second registry.

Credit for the feature, the CLAUDE_CONFIG_DIR diagnosis, and defect B is @hannin / #162 — added to the description.

No French strings; #185 can pick them up on rebase.

swift test: 760 pass, 10 skipped.

@chattymin
chattymin merged commit 9bb7dee into chattymin:main Aug 22, 2026
2 checks passed
@chattymin

Copy link
Copy Markdown
Owner

Thank you for your contribution!

cte13 pushed a commit to cte13/PokeTokenBar-iOS that referenced this pull request Aug 23, 2026
Brings in upstream features and integrates them with the fork's iOS/OpenCode Go work:
- Antigravity 2.0 / IDE official rate-limit tracking (chattymin#210, chattymin#219, chattymin#220)
- French (fr) UI language (chattymin#185) — added fr strings to fork-only labels
- Per-provider additional scan folders (chattymin#177/chattymin#187)
- Account-labeled official limits (chattymin#199), swift.org Swift 6.3 toolchains (chattymin#216),
  claude-fable-5 pricing (chattymin#198)

Conflicts resolved in Localization, UsageStore, PopoverView, and their tests by
keeping both the fork's OpenCode Go limits and upstream's Antigravity limits, and
by giving fork-only localized labels their French values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chattymin added a commit that referenced this pull request Aug 23, 2026
…ers (en/ko/ja)

Three UI features have shipped since v2.5.1 without an image: pinning a
representative Pokémon (#158), official rate limits for Antigravity (#210), and
per-provider additional scan folders (#187). Per-provider scan folders were not
mentioned in the READMEs at all.

Adds two screenshots per language for the Antigravity limits panel and the
Advanced scan-folder controls, and inserts the Representative Pokémon row into
the existing settings shots rather than redrawing them, so every pixel outside
the inserted row is byte-identical to the previous asset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
chattymin added a commit that referenced this pull request Aug 24, 2026
…ers (en/ko/ja) (#223)

Three UI features have shipped since v2.5.1 without an image: pinning a
representative Pokémon (#158), official rate limits for Antigravity (#210), and
per-provider additional scan folders (#187). Per-provider scan folders were not
mentioned in the READMEs at all.

Adds two screenshots per language for the Antigravity limits panel and the
Advanced scan-folder controls, and inserts the Representative Pokémon row into
the existing settings shots rather than redrawing them, so every pixel outside
the inserted row is byte-identical to the previous asset.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
lukemonaghan pushed a commit to lukemonaghan/PokeTokenBar that referenced this pull request Aug 24, 2026
…ymin#187)

* feat: add per-provider additional scan folders (chattymin#177)

Logs outside the built-in paths were a silent undercount for every
provider except Claude. Store one list per provider so parsers never
share folders, and only add custom roots — never evict a curated default.

* fix: union custom scan roots onto every Antigravity default (chattymin#177)

resolvedRoots hardcoded the CLI path, which is defaultRoots[1] after
chattymin#182, so a no-root scan dropped 2.0/Core and IDE stores. Codex custom
roots had the same class of hole against archived_sessions (chattymin#181).
lukemonaghan pushed a commit to lukemonaghan/PokeTokenBar that referenced this pull request Aug 24, 2026
…ers (en/ko/ja) (chattymin#223)

Three UI features have shipped since v2.5.1 without an image: pinning a
representative Pokémon (chattymin#158), official rate limits for Antigravity (chattymin#210), and
per-provider additional scan folders (chattymin#187). Per-provider scan folders were not
mentioned in the READMEs at all.

Adds two screenshots per language for the Antigravity limits panel and the
Advanced scan-folder controls, and inserts the Representative Pokémon row into
the existing settings shots rather than redrawing them, so every pixel outside
the inserted row is byte-identical to the previous asset.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Borisserz added a commit to Borisserz/PokeTokenBar that referenced this pull request Sep 2, 2026
…ymin#200)

Gemini/Codex estimates ride other subscriptions, so mixing them into
the Max multiple inflates it. Settings was writing UserDefaults on
every keystroke; match the chattymin#187 draft+commit field.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Extend additional scan folders to every provider, not just Claude

2 participants